﻿using System;
using System.Text;
using System.Threading;
using System.Xml;
using System.IO;
using System.Linq;

namespace UPNP.IO
{
	/// <summary>
	/// used to create a device schema
	/// </summary>
	public sealed partial class DeviceSchema
	{
		public const int SchemaVersion = 3;	// since v2.0.0.0

        public static readonly Encoding UTF8EncodingNoBOM = new UTF8Encoding(false);

        private readonly log4net.ILog _log = log4net.LogManager.GetLogger(typeof(DeviceSchema));
	    private readonly Lazy<RootDevice> _createdRootDevice;
	    private readonly Uri _rootDeviceSchemaUrl;
        private readonly XmlDocument _schema;
        private readonly XmlElement _root;
	    private readonly XmlAttribute _schemaIdAttribute;

	    public string SchemaId
        {
            get; private set;
        }

        public string UniqueDeviceName
        {
            get; private set;
        }

        public string Type
        {
            get;
            private set;
        }

	    public Uri RootDeviceSchemaUrl
	    {
            get { return _rootDeviceSchemaUrl; }
        }

        #region xml serialization

        public string ToXml()
		{
		    return _schema.OuterXml;
		}

        public void ToXml(Stream targetStream)
        {
            using (var xw = new XmlTextWriter(targetStream, UTF8EncodingNoBOM))
            {
                xw.Formatting = Formatting.Indented;

                _schema.WriteTo(xw);
            }
        }

        private void WriteXmlToTempFile(bool throwOnError = true)
        {
            try
            {
                var f = new FileInfo(Path.Combine(Path.GetTempPath(), "DeviceSchema_" + this.SchemaId + ".xml"));

                if (f.Exists)
                {
                    _log.Info("Dumped Device Schema to '" + f.FullName + "'");
                }
                else
                {
                    _log.Info("Dumping Device Schema to '" + f.FullName + "' ...");

                    using (var s = f.Create())
                    {
                        ToXml(s);
                    }
                }
            }
            catch (Exception x)
            {
                if (throwOnError)
                {
                    _log.Error("Failed to dump device schema", x);

                    throw;
                }

                // do not throw
                if (_log.IsWarnEnabled)
                    _log.Warn("Failed to dump device schema", x);
            }
        }

        #endregion

        /// <summary>
        /// creates the root device (and its children and services) for this schema
        /// </summary>
        public RootDevice CreateRootDevice()
        {
            return _createdRootDevice.Value;
        }

        private RootDevice CreateRootDeviceInternal()
        {
            if (_log.IsInfoEnabled)
            {
                _log.InfoFormat(
                    "Creating managed root device for device {0} ({1})",
                    this.UniqueDeviceName,
                    this.Type
                    );
            }

            // create device dom
            DeviceDom dd = null;

            try
            {
                // gather root device
                var rootDevice = (XmlElement)_schema.SelectSingleNode(
                    "/schema/deviceDescription[@ref='#" + 
                    this.UniqueDeviceName +
                    "']/*[local-name()='root']/*[local-name()='device']"
                    );

                dd = new DeviceDom(_rootDeviceSchemaUrl, rootDevice);

                // finalize
                _log.InfoFormat(
                    "Generated device DOM for device {0} ({1})",
                    this.UniqueDeviceName,
                    this.Type
                    );

                return new RootDevice(this, dd);
            }
            catch (FaultyImplementationException e)
            {
                _log.Error(string.Format(
                    "Failed to create managed root device for device {0} ({1})",
                    this.UniqueDeviceName,
                    this.Type
                    ), e);

                // dump device schema on faulty implementation
                if (_log.IsDebugEnabled)
                {
                    WriteXmlToTempFile(false);
                }

                throw;
            }
            catch (Exception e)
            {
                _log.Error(string.Format(
                    "Failed to create managed root device for device {0} ({1})",
                    this.UniqueDeviceName,
                    this.Type
                    ), e);

                throw;
            }
            finally
            {
                // dump all schema errors
                if ((dd != null) && _log.IsWarnEnabled)
                {
                    var err = dd.GetSchemaErrorsRecursive().ToArray();

                    if (err.Length > 0)
                    {
                        _log.Warn("--------------------------------------------- DEVICE SCHEMA ERRORS - START ------------------------------------");
                        _log.Warn("Device Schema contains " + err.Length + " errors: " + this.SchemaId);

                        WriteXmlToTempFile(false);

                        // dump
                        foreach (var e in err)
                        {
                            _log.Warn(e.Message, e);
                        }

                        _log.Warn("--------------------------------------------- DEVICE SCHEMA ERRORS - END ------------------------------------");
                    }
                    else
                    {
                        if (_log.IsDebugEnabled)
                        {
                            _log.Debug("Device Schema is perfectly valid: " + this.SchemaId);
                        }
                    }
                }
            }
        }

		public DeviceSchema(Uri rootDeviceSchemaUrl)
		{
            if (rootDeviceSchemaUrl == null)
                throw new ArgumentNullException("rootDeviceSchemaUrl");

            _rootDeviceSchemaUrl = rootDeviceSchemaUrl;

            _createdRootDevice = new Lazy<RootDevice>(
                CreateRootDeviceInternal,
                true
                );

			_schema = new XmlDocument();

			// declaration
			_schema.AppendChild(_schema.CreateXmlDeclaration("1.0", "utf-8", "no"));

			// create root node
			_root = _schema.CreateElement("schema");

			_schema.AppendChild(_root);
			
			{
			    _schemaIdAttribute = _schema.CreateAttribute("id");

			    _schemaIdAttribute.Value = string.Empty;

				_root.Attributes.Append(_schemaIdAttribute);
			}

			{
				var a = _schema.CreateAttribute("version");

				a.Value = SchemaVersion.ToString();

				_root.Attributes.Append(a);
			}

			// download device schema
		    DownloadDeviceDescription();
		}
	}
}
